package com.doodle.common.utils;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.graphics.Canvas;
import android.graphics.Matrix.ScaleToFit;
import android.graphics.Paint;
import android.graphics.PorterDuff.Mode;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.util.Log;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;

public class BitmapUtils {
	
	public static final int FIT_LENGTH_SMALLER = 0;
	public static final int FIT_LENGTH_LARGGER = 1;
	public static final int FIT_LENGTH_SCALE_FILL = 2;
	public static final int FIT_LENGTH_SCALE_START = 3;
	public static final int FIT_LENGTH_SCALE_CENTER = 4;
	public static final int FIT_LENGTH_SCALE_END = 5;
	public static final int FIT_PIXEL_COUNT = 6;
	
	private static final ScaleToFit[] sS2FArray = {
        ScaleToFit.FILL,
        ScaleToFit.START,
        ScaleToFit.CENTER,
        ScaleToFit.END
    };
    
	public static Bitmap loadBitmapFromFile(String filePath, int width, int height, int fitMode, int poolType, int radius) {
		return loadBitmapFromFile(ThreadPool.JOB_CONTEXT_STUB, filePath, width, height, fitMode, poolType, radius);
	}
	
    public static Bitmap loadBitmapFromFile(ThreadPool.JobContext jc, String filePath, int width, int height, int fitMode, int poolType, int radius) {
    	ParcelFileDescriptor fileDescriptor = null;
    	try {
    		fileDescriptor = ParcelFileDescriptor.open(new File(filePath), ParcelFileDescriptor.MODE_READ_ONLY);
    		return decodeBitmap(jc, fileDescriptor.getFileDescriptor(), width, height, fitMode, poolType, radius);
    	 } catch (FileNotFoundException e) {
    		 e.printStackTrace();
    	 } finally {
              Utils.closeSilently(fileDescriptor);
    	 }
    	 return null;
    }
    
    public static Bitmap loadBitmapFromUri(Context context, Uri uri, int width, int height, int fitMode, int poolType, int radius) {
    	return loadBitmapFromUri(ThreadPool.JOB_CONTEXT_STUB, context, uri, width, height, fitMode, poolType, radius);
    }
    
    public static Bitmap loadBitmapFromUri(ThreadPool.JobContext jc, Context context, Uri uri, int width, int height, int fitMode, int poolType, int radius) {
    	ParcelFileDescriptor fileDescriptor = null;
    	try {
    		fileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r");
    		return decodeBitmap(jc, fileDescriptor.getFileDescriptor(), width, height, fitMode, poolType, radius);
    	 } catch (FileNotFoundException e) {
    		 e.printStackTrace();
    	 } finally {
              Utils.closeSilently(fileDescriptor);
    	 }
    	 return null;
    }
	
	private static class DecodeCanceller implements ThreadPool.CancelListener {
        Options mOptions;

        public DecodeCanceller(Options options) {
            mOptions = options;
        }

        @Override
        public void onCancel() {
            mOptions.requestCancelDecode();
        }
    }
	
	public static Bitmap decodeBitmap(ThreadPool.JobContext jc, byte[] data, int width, int height, int fitMode, int poolType, int radius) {
		return decodeBitmap(jc, null, data, width, height, fitMode, poolType, radius);
	}
    
	public static Bitmap decodeBitmap(ThreadPool.JobContext jc, FileDescriptor fd, int width, int height, int fitMode, int poolType, int radius) {
		return decodeBitmap(jc, fd, null, width, height, fitMode, poolType, radius);
	}
	
    public static Bitmap decodeBitmap(ThreadPool.JobContext jc, FileDescriptor fd, byte[] data, int width, int height, int fitMode, int poolType, int radius) {
        Options options = new Options();
        jc.setCancelListener(new DecodeCanceller(options));

        options.inJustDecodeBounds = true;
        if (fd != null) {
        	BitmapFactory.decodeFileDescriptor(fd, null, options);
        } else if (data != null) {
        	BitmapFactory.decodeByteArray(data, 0, data.length, options);
        }
        if (jc.isCancelled()) return null;
        
        double scale = 1.0;

        int w = options.outWidth;
        int h = options.outHeight;
        
        if (fitMode == FIT_PIXEL_COUNT && w * h > 0 && w * h > width * height) {
    		scale = Math.sqrt(width * height / (double)(w * h));
    	}
        
        int lw = width;
        int lh = height;

        options.inSampleSize = 1;
        if (lw > 0 && lh > 0) {
        	if (fitMode == FIT_LENGTH_SMALLER) {
        		while (w > lw || h > lh) {
    				lw *= 2;
    				lh *= 2;
    				options.inSampleSize *= 2;
    			}
        	} else {
	        	lw *= 2;
	        	lh *= 2;
    			while (w >= lw || h >= lh) {
    				lw *= 2;
    				lh *= 2;
    				options.inSampleSize *= 2;
    			}
        	}
        }
        
        options.inJustDecodeBounds = false;
        options.inMutable = true;
        
    	BitmapPool pool = null;
        if (options.inSampleSize == 1) {
        	pool = BitmapPool.getPool(poolType);
        	options.inBitmap = pool.getBitmap(w, h);
        }

        Bitmap bitmap = null;
        try {
			jc.setMode(ThreadPool.MODE_CPU);
			if (fd != null) {
				bitmap = BitmapFactory.decodeFileDescriptor(fd, null, options);
	        } else if (data != null) {
	        	bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);
	        }
			if (pool != null && options.inBitmap != null) {
				if (options.inBitmap != bitmap) {
					pool.recycle(options.inBitmap);
					options.inBitmap = null;
				}
			}
        } catch (IllegalArgumentException e) {
            if (options.inBitmap == null) {
            	throw e;
            }
            Log.w("Utils", "decode fail with a given bitmap, try decode to a new bitmap");
            pool.recycle(options.inBitmap);
            options.inBitmap = null;
            if (fd != null) {
				bitmap = BitmapFactory.decodeFileDescriptor(fd, null, options);
	        } else if (data != null) {
	        	bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);
	        }
        } finally {
			jc.setMode(ThreadPool.MODE_NONE);
		}
        
        /*Log.d("XXX", String.format("decode: %d, %d, %d, %d, %d, %d, %d, %.2f, %.2f, %d, %.2f",
        		w, h, width, height, options.outWidth, options.outHeight,
        		options.inSampleSize, (float)options.outWidth / w, (float)options.outWidth / width,
        		options.outWidth * options.outHeight * 4, bitmap == null ? 0 : bitmap.getByteCount() / 1E6));*/
        
        if (fitMode == FIT_PIXEL_COUNT && bitmap != null && scale != 1.0) {
        	w = (int)(w * scale);
        	h = (int)(h * scale);
        	bitmap = getScaledBitmap(bitmap, w, h, 0, ScaleToFit.FILL, poolType, true);
    	}
        
        Bitmap scaledBitmap = null;
        if (bitmap != null) {
	        if (((fitMode >= FIT_LENGTH_SCALE_FILL && fitMode <= FIT_LENGTH_SCALE_END) && (bitmap.getWidth() != width || bitmap.getHeight() != height)) || radius > 0) {
	        	if (fitMode < FIT_LENGTH_SCALE_FILL || fitMode > FIT_LENGTH_SCALE_END) {
	        		fitMode = FIT_LENGTH_SCALE_CENTER;
	        	}
	        	scaledBitmap = getScaledBitmap(bitmap, width, height, radius, sS2FArray[fitMode - FIT_LENGTH_SCALE_FILL], poolType, true);
	        }
        }
        
        return scaledBitmap == null ? bitmap : scaledBitmap;
    }
    
	public static int getBitmapOrientation(String filePath) {
		int degree = 0;
		try {
			ExifInterface exif = new ExifInterface(filePath);
			if (exif != null) {
				int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1);
				switch (orientation) {
				case ExifInterface.ORIENTATION_ROTATE_90:
					degree = 90;
					break;
				case ExifInterface.ORIENTATION_ROTATE_180:
					degree = 180;
					break;
				case ExifInterface.ORIENTATION_ROTATE_270:
					degree = 270;
					break;
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return degree;
	}
    
    public static int getBitmapOrientation(Context context, Uri uri) {
    	int rotation = 0;
    	InputStream instream = null;
    	try {
    		instream = context.getContentResolver().openInputStream(uri);
    		rotation = getBitmapOrientation(instream);
    	} catch (FileNotFoundException e) {
   		 	e.printStackTrace();
   	 	} finally {
   	 		Utils.closeSilently(instream);
   	 	}
   	 	return rotation;
    }

    private static boolean read(InputStream is, byte[] buf, int length) {
        try {
            return is.read(buf, 0, length) == length;
        } catch (IOException ex) {
            return false;
        }
    }

    private static int pack(byte[] bytes, int offset, int length,
            boolean littleEndian) {
        int step = 1;
        if (littleEndian) {
            offset += length - 1;
            step = -1;
        }

        int value = 0;
        while (length-- > 0) {
            value = (value << 8) | (bytes[offset] & 0xFF);
            offset += step;
        }
        return value;
    }
    
    public static int getBitmapOrientation(InputStream is) {
        if (is == null) {
            return 0;
        }

        byte[] buf = new byte[8];
        int length = 0;

        // ISO/IEC 10918-1:1993(E)
        while (read(is, buf, 2) && (buf[0] & 0xFF) == 0xFF) {
            int marker = buf[1] & 0xFF;

            // Check if the marker is a padding.
            if (marker == 0xFF) {
                continue;
            }

            // Check if the marker is SOI or TEM.
            if (marker == 0xD8 || marker == 0x01) {
                continue;
            }
            // Check if the marker is EOI or SOS.
            if (marker == 0xD9 || marker == 0xDA) {
                return 0;
            }

            // Get the length and check if it is reasonable.
            if (!read(is, buf, 2)) {
                return 0;
            }
            length = pack(buf, 0, 2, false);
            if (length < 2) {
                return 0;
            }
            length -= 2;

            // Break if the marker is EXIF in APP1.
            if (marker == 0xE1 && length >= 6) {
                if (!read(is, buf, 6)) return 0;
                length -= 6;
                if (pack(buf, 0, 4, false) == 0x45786966 &&
                    pack(buf, 4, 2, false) == 0) {
                    break;
                }
            }

            // Skip other markers.
            try {
                is.skip(length);
            } catch (IOException ex) {
                return 0;
            }
            length = 0;
        }

        // JEITA CP-3451 Exif Version 2.2
        if (length > 8) {
            int offset = 0;
            byte[] jpeg = new byte[length];
            if (!read(is, jpeg, length)) {
                return 0;
            }

            // Identify the byte order.
            int tag = pack(jpeg, offset, 4, false);
            if (tag != 0x49492A00 && tag != 0x4D4D002A) {
                return 0;
            }
            boolean littleEndian = (tag == 0x49492A00);

            // Get the offset and check if it is reasonable.
            int count = pack(jpeg, offset + 4, 4, littleEndian) + 2;
            if (count < 10 || count > length) {
                return 0;
            }
            offset += count;
            length -= count;

            // Get the count and go through all the elements.
            count = pack(jpeg, offset - 2, 2, littleEndian);
            while (count-- > 0 && length >= 12) {
                // Get the tag and check if it is orientation.
                tag = pack(jpeg, offset, 2, littleEndian);
                if (tag == 0x0112) {
                    // We do not really care about type and count, do we?
                    int orientation = pack(jpeg, offset + 8, 2, littleEndian);
                    switch (orientation) {
                        case 1:
                            return 0;
                        case 3:
                            return 180;
                        case 6:
                            return 90;
                        case 8:
                            return 270;
                    }
                    return 0;
                }
                offset += 12;
                length -= 12;
            }
        }
        return 0;
    }
	
	private static final String[] EXIF_TAGS = {
		ExifInterface.TAG_DATETIME,
		ExifInterface.TAG_MAKE,
		ExifInterface.TAG_MODEL,
		ExifInterface.TAG_FLASH,
		ExifInterface.TAG_GPS_LATITUDE,
		ExifInterface.TAG_GPS_LONGITUDE,
		ExifInterface.TAG_GPS_LATITUDE_REF,
		ExifInterface.TAG_GPS_LONGITUDE_REF,
		ExifInterface.TAG_GPS_ALTITUDE,
		ExifInterface.TAG_GPS_ALTITUDE_REF,
		ExifInterface.TAG_GPS_TIMESTAMP,
		ExifInterface.TAG_GPS_DATESTAMP,
		ExifInterface.TAG_WHITE_BALANCE,
		ExifInterface.TAG_FOCAL_LENGTH,
		ExifInterface.TAG_GPS_PROCESSING_METHOD
	};
	
	private static SimpleDateFormat DATE_TAKEN_FORMAT = new SimpleDateFormat("yyyy:MM:dd kk:mm:ss");
	
	public static void copyExif(String source, String destination, int newWidth, int newHeight, boolean keepTime) {
        try {
            ExifInterface oldExif = new ExifInterface(source);
            ExifInterface newExif = new ExifInterface(destination);

            newExif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, String.valueOf(newWidth));
            newExif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, String.valueOf(newHeight));
            newExif.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(0));

            for (String tag : EXIF_TAGS) {
                String value = oldExif.getAttribute(tag);
                if (value != null) {
                    newExif.setAttribute(tag, value);
                }
            }
            
            if (!keepTime) {
            	long dateTaken = System.currentTimeMillis();
            	newExif.setAttribute(ExifInterface.TAG_DATETIME, DATE_TAKEN_FORMAT.format(dateTaken));
            }

            String value = oldExif.getAttribute(ExifInterface.TAG_APERTURE);
            if (value != null) {
                try {
                    float aperture = Float.parseFloat(value);
                    newExif.setAttribute(ExifInterface.TAG_APERTURE,
                            String.valueOf((int) (aperture * 10 + 0.5f)) + "/10");
                } catch (NumberFormatException e) {
                	e.printStackTrace();
                }
            }
            newExif.saveAttributes();
        } catch (Throwable t) {
        	t.printStackTrace();
        }
    }
	
	public static Bitmap createBitmap(int width, int height, int poolType, Config config) {
		Bitmap output = BitmapPool.getPool(poolType).getBitmap(width, height);
        Config cfg = output == null ? null : output.getConfig();
        if (cfg == null || cfg != config) {
        	output = Bitmap.createBitmap(width, height, config);
        }
        return output;
	}
	
	public static Bitmap getScaledBitmap(Bitmap bitmap, int width, int height, int radius, ScaleToFit scaleToFit, int poolType, boolean recycle) {
		if (bitmap == null) {
			return null;
		}
		
        Bitmap output = BitmapPool.getPool(poolType).getBitmap(width, height);
        Config cfg = output == null ? null : output.getConfig();
        
        if (cfg == null || cfg != Config.ARGB_8888) {
        	output = Bitmap.createBitmap(width, height, Config.ARGB_8888);
        }
        
        if(output != null){
            Canvas canvas = new Canvas(output);
            drawBitmapScaleToFit(canvas, 0, 0, width, height, bitmap, radius, scaleToFit);
        }
        
        if (recycle) {
        	BitmapPool.getPool(poolType).recycle(bitmap);
        }
        return output;
    }

	public static void drawBitmapScaleToFit(Canvas canvas, int x, int y, int width, int height, Bitmap bitmap, int radius, ScaleToFit scaleToFit) {
		Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setFilterBitmap(true);
        
        int bmWidth = bitmap.getWidth();
        int bmHeight = bitmap.getHeight();
        
        int left = 0;
        int top = 0;
        int right = bmWidth;
        int bottom = bmHeight;
        
        if (scaleToFit != ScaleToFit.FILL) {
            int srcWidth = bmWidth;
            int srcHeight = bmHeight;
            
            int compareLeft = srcWidth * height;
            int compareRight = width * srcHeight;
	        if (compareLeft > compareRight) {
	        	srcWidth = Math.round((float)compareRight / height);
	        	left = (bmWidth - srcWidth) / 2;
        		right = left + srcWidth;
	        } else if (compareLeft < compareRight) {
	        	srcHeight = Math.round((float)compareLeft / width);
	        	if (scaleToFit == ScaleToFit.START) {
	        		bottom = srcHeight;
	        	} else if (scaleToFit == ScaleToFit.CENTER) {
	        		top = (bmHeight - srcHeight) / 2;
	        		bottom = top + srcHeight;
	        	} else {
	        		top = bmHeight - srcHeight;
	        		bottom = bmHeight;
	        	}
	        }
        }
        
        final Rect rect = new Rect(left, top, right, bottom);
        final RectF rectF = new RectF(x, y, width, height);
        
        if (radius > 0) {
        	paint.setColor(0xff424242);
        	canvas.drawARGB(0, 0, 0, 0);
        	paint.setXfermode(null);
        	canvas.drawRoundRect(rectF, radius, radius, paint);
        	paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
        }
        
        canvas.drawBitmap(bitmap, rect, rectF, paint);
	}
}
